Skip to content

staticaddr/withdraw: prepare deposits before publish#1176

Open
hieblmi wants to merge 1 commit into
lightninglabs:masterfrom
hieblmi:fix-htlc-timeout
Open

staticaddr/withdraw: prepare deposits before publish#1176
hieblmi wants to merge 1 commit into
lightninglabs:masterfrom
hieblmi:fix-htlc-timeout

Conversation

@hieblmi

@hieblmi hieblmi commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Neutrino can remove a spent input from its wallet view while transaction
publication is still blocked. If the deposit remains Deposited,
reconciliation can remove it before the withdrawal manager records the
withdrawal intent.

Attach the finalized transaction, transition the deposits to Withdrawing,
and set up local tracking before publishing. Keep the prepared state on
publication errors because the wallet RPC outcome can be ambiguous and
recovery or fee bumping must be able to retry.

@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request improves the reliability of the loop-in process by ensuring that selected deposits are always retrieved from the live, canonical state managed by the deposit manager. By moving away from potentially stale database snapshots, the system avoids incorrect state validation and prevents failures during timeout transitions.

Highlights

  • Deposit Refresh Logic: Updated the loop-in deposit refresh mechanism to use the deposit manager's active set instead of database snapshots, ensuring state consistency.
  • API Integration: Switched from DepositsForOutpoints to AllStringOutpointsActiveDeposits to enforce active state validation during recovery.
  • Test Suite Updates: Refactored mock deposit managers and test cases to reflect the new active deposit lookup requirements.
New Features

🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the refreshSelectedDeposits function in staticaddr/loopin/actions.go to retrieve active deposits using the deposit manager's AllStringOutpointsActiveDeposits method instead of DepositsForOutpoints. This ensures that recovery relies on the deposit manager's active set rather than stale snapshots. The corresponding unit tests and the noopDepositManager mock in actions_test.go have been updated to reflect and verify this change. There are no review comments, and I have no additional feedback to provide.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@hieblmi
hieblmi force-pushed the fix-htlc-timeout branch 4 times, most recently from 695f614 to 12cd993 Compare July 20, 2026 14:20
@hieblmi
hieblmi requested a review from starius July 20, 2026 19:16
Neutrino can remove a spent input from its wallet view while transaction
publication is still blocked. If the deposit remains Deposited,
reconciliation can remove it before the withdrawal manager records the
withdrawal intent.

Attach the finalized transaction, transition the deposits to Withdrawing,
and set up local tracking before publishing. Keep the prepared state on
publication errors because the wallet RPC outcome can be ambiguous and
recovery or fee bumping must be able to retry.
@hieblmi
hieblmi force-pushed the fix-htlc-timeout branch from 12cd993 to 9ee5c42 Compare July 20, 2026 19:18
@hieblmi hieblmi changed the title staticaddr/loopin: keep refreshed deposits canonical staticaddr/withdraw: prepare deposits before publish Jul 20, 2026

@starius starius left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found some potentially fragile parts in this branch. I haven't validated them empirically, so some of them may be false-positives.

Also I propose to add regression coverage in a separate commit. If the main fix commit is then reverted, the new test must build and run, but fail with a symptomatic error, reproducing the failure you are fixing. So it is easy to demo that the fix is sound.

deposits[0].Lock()
prevTx := deposits[0].FinalizedWithdrawalTx
deposits[0].Unlock()
previousWithdrawalTx := previousWithdrawalTxns[0]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assumption is pre-existing, but could we fix it while touching this logic? The fee-bump path verifies that all Withdrawing deposits reference the same previous transaction, but that invariant is not enforced for Deposited deposits or inconsistent recovery states.

Since we now retain every previous transaction, consider deleting every distinct non-nil previous hash other than finalizedTx, or explicitly validating that all entries match. Otherwise, a stale transaction associated with a deposit other than index 0 could remain in finalizedWithdrawalTxns and continue being republished. A multi-deposit regression test would be useful here.

withdrawalPkScript, err := txscript.PayToAddrScript(withdrawalAddress)
// Transition before publishing so wallet reconciliation can't remove a
// spent deposit from the active set while publication is in progress.
err = m.cfg.DepositManager.TransitionDeposits(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we ensure persistence errors are observable here? TransitionDeposits returns success even when the FSM’s Store.UpdateDeposit fails, because that error is only logged. Since the explicit update now only runs for fee bumps, an initial withdrawal can reach publication while the database still says Deposited and has no finalized transaction. That recreates the restart/reconciliation race this change is intended to fix.

if err != nil {
return "", "", fmt.Errorf("could not get withdrawal "+
"pkscript: %w", err)
for i, d := range deposits {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this rollback safe for multiple deposits? TransitionDeposits processes FSMs sequentially, so an earlier deposit may already be Withdrawing and persisted when a later transition fails. Restoring only FinalizedWithdrawalTx would leave that deposit in Withdrawing with its old or nil transaction, while the cluster may have mixed states. We likely need atomic persistence/transition or a rollback that also restores state and database records.

err = m.handleWithdrawal(
ctx, deposits, finalizedTx.TxHash(), withdrawalPkScript,
)
if err != nil {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point the deposits are already Withdrawing. If handleWithdrawal fails, we return before publishing or adding local tracking. A retry is then classified as a fee bump and skips handleWithdrawal, so the replacement can publish without any spend/confirmation watcher and remain Withdrawing until restart. Could this path retry notifier setup or restore the prepared state before returning?


// Add the new withdrawal tx to the finalized withdrawals to republish
// it on block arrivals.
m.mu.Lock()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add the replacement to this map only after all deposit updates succeed? If an UpdateDeposit below fails, the method returns but the old transaction has already been removed and the replacement remains eligible for block-triggered republication. That can broadcast a transaction whose database records are old or only partially updated.

// notifier is run.
if allDeposited {
// Persist info about the finalized withdrawal.
err = m.cfg.Store.CreateWithdrawal(ctx, deposits)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should creating the withdrawal record be part of the durable preparation and return its error? The deposits are already persisted as Withdrawing, so a crash or ignored error here allows recovery to publish the transaction without a corresponding withdrawal record. Confirmation then cannot update that record, leaving the withdrawal permanently absent from history.

@hieblmi hieblmi self-assigned this Jul 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants